/**
 * 维护聊天“元数据”，如联系人的未读消息数、预警记录
 */
import { truthy, Service } from '@gnlab/utils'
import { MemSubject, mmap } from '@gnlab/utils/rx'
import { OrderedMap } from 'immutable'
import { playNotifyAudio } from '@/lib/notify'
import type { Services } from '@/services'
import { API } from '@/services/common/api'
import { ChatRecordType } from '@/services/contact-chat'
import type { InProgressInfo } from '@/services/contact-chat'
import type { ContactTypeService } from '@/services/contact-types/common/ContactTypeService'
import {
  type Contact,
  type ContactType,
  makeConversationId,
  splitContactId,
} from '@/services/contacts'
import { resetUnread, clearWarn, markUnread } from '../api'
import type { UnreadItem, WarnItem } from './definition'
import { makeUnreadItem, makeEmptyWarnItem, logger } from './definition'

export * from './definition'

/**
 * ======================
 * Service
 * ======================
 */
export class Metas extends Service<Services> {
  get auth() {
    return this.services.auth
  }
  private get contacts() {
    return this.services.contacts
  }
  private get currentContact() {
    return this.services.contacts.current.contact.value
  }
  private get currentId() {
    return this.services.contacts.current.id
  }
  private get externals() {
    return this.services.externals
  }
  private get transfers() {
    return this.services.transfers
  }
  private get groups() {
    return this.services.groups
  }
  private get internals() {
    return this.services.internals
  }
  private get notices() {
    return this.services.notices
  }
  private get chat() {
    return this.services.chat
  }
  private get chatInfo() {
    return this.services.chat.inProgress.info
  }
  private get observingReadonly() {
    return this.services.auth.observing.readonly$.value
  }

  private readonly _unreads = new MemSubject(OrderedMap<string, UnreadItem>())
  readonly unreads = this._unreads.asStream()

  private readonly _warns = new MemSubject(OrderedMap<string, WarnItem>())
  readonly warns = this._warns.asStream() // 预警记录列表（经过排序，最后更新的在最前面）

  // 预警记录总数
  readonly warnTotalCount = this.warns.pipe(
    mmap(warns => warns.reduce((sum, item) => sum + item.count, 0)),
  )

  registered() {
    void this.auth.bizAccountIds.then(() => {
      this.initSyncWithContacts()

      this.initUpdateUnread()
      this.initResetUnreadByCurrent()
      this.initResetUnreadByChatPending()

      this.initRecordWarns()
    })
  }

  /**
   * ===================================
   * 基于联系人列表，维护 meta 信息
   * ===================================
   */

  initSyncWithContacts() {
    for (const source of [
      this.externals,
      this.groups,
      this.internals,
      this.notices,
    ] as ContactTypeService<Contact>[]) {
      source.added.subscribe(this.padByContacts.bind(this))
      source.removed.subscribe(this.removeByContacts.bind(this))
    }

    this.transfers.loaded.subscribe(this.padByContacts.bind(this))
    this.transfers.backed.subscribe(contact => this.removeByContacts([contact]))
    this.transfers.returned.subscribe(contact => {
      // 如果此联系人不存在于客户列表里，将其 meta 移除
      if (!this.externals.getById(contact.id)) {
        this.removeByContacts([contact])
      }
    })
  }
  private padByContacts(contacts: Contact[]) {
    let unreads = this.unreads.value
    let warns = this.warns.value
    for (const contact of contacts) {
      if (contact.initialUnread) {
        const unread = unreads.get(contact.id)
        if (!unread) {
          unreads = unreads.set(contact.id, {
            ...makeUnreadItem(contact.id, contact.type),
            unread: contact.initialUnread,
          })
        } else if (unread.type === null) {
          // 填充缺失的 type，此情况不使用新返回的 initialUnread，沿用本地记录的未读消息数
          unreads = unreads.set(contact.id, { ...unread, type: contact.type })
        }
      }
      if (contact.initialWarn && !warns.has(contact.id)) {
        warns = warns.set(contact.id, contact.initialWarn)
      }
    }
    if (unreads !== this.unreads.value) this._unreads.next(unreads)
    if (warns !== this.warns.value) {
      this._warns.next(
        warns.sort((a, b) => b.updated - a.updated), // 按更新时间排序
      )
    }
  }
  private removeByContacts(contacts: Contact[]) {
    let unreads = this.unreads.value
    let warns = this.warns.value
    for (const contact of contacts) {
      unreads = unreads.remove(contact.id)
      warns = warns.remove(contact.id)
    }
    if (unreads !== this.unreads.value) this._unreads.next(unreads)
    if (warns !== this.warns.value) this._warns.next(warns)
  }

  /**
   * ===================================
   * 维护未读消息数
   * ===================================
   */

  /**
   * 在收到消息时更新联系人的未读消息数
   *
   * - 自己发的消息（无论是本机发出还是其他设备发出）：跳过
   * - 加好友成功的通知消息：跳过
   * - 当前联系人
   *   - chat pending 状态：+1
   *   - 非 pending 状态：重置未读数
   * - 非当前联系人：+1
   */
  private initUpdateUnread() {
    this.chat.received.subscribe(items => {
      Object.entries(items).forEach(([contactId, { contact, records }]) => {
        const filtered = records.filter(
          record =>
            !record.isSend &&
            (record.type !== ChatRecordType.Text ||
              record.content.text !== '我通过了你的联系人验证请求，现在我们可以开始聊天了'),
        )
        if (!filtered.length) return

        const current = this.currentContact // 当前联系人（不一定是收到消息的联系人）
        const contactIsCurrent = current?.id === contactId
        if (contactIsCurrent) {
          if (truthy(this.chatInfo.value?.pending)) {
            this.updateUnread(current, records.length)
          } else {
            this.resetUnread([current])
          }
        } else {
          this.updateUnread(contact, records.length)
        }

        // 有新的未读消息，播放通知
        const muted = this.groups.mutes.value.has(contact.id)
        if ((document.hidden || !contactIsCurrent) && !muted) void playNotifyAudio()
      })
    })
  }

  /**
   * 联系人未变化而会话的 pending 状态被重置时，同步重置其未读消息数（因为当前会话在 pending 状态下会统计未读消息数）
   */
  private initResetUnreadByChatPending() {
    let prev: { contactId: string; chatInfo: InProgressInfo } | null = null
    this.chatInfo.subscribe(chatInfo => {
      const current = this.currentContact
      if (
        // 数据齐全
        current !== null &&
        prev &&
        chatInfo &&
        // 联系人未变化
        prev.contactId === current.id &&
        // pending 状态从“有记录”重置为“没有记录”
        typeof prev.chatInfo.pending === 'string' &&
        typeof chatInfo.pending !== 'string'
      ) {
        this.resetUnread([current])
      }
      prev =
        current !== null && chatInfo
          ? {
              contactId: current.id,
              chatInfo,
            }
          : null
    })
  }

  /**
   * 在“当前联系人”切换时重置未读消息数
   */
  private initResetUnreadByCurrent() {
    this.currentId.subscribe(id => {
      if (id !== null && this.unreads.value.has(id)) {
        const contact = this.contacts.getById(id)
        if (contact) this.resetUnread([contact])
      }
    })
  }

  /**
   * 更新本地记录的未读消息数
   *
   * 若 replace=false，会在原未读消息数基础上加或减 update
   * 若 replace=true，则直接用 update 作为新的未读数
   */
  private updateUnread(
    contact: { id: string; conversationId: string; type?: ContactType },
    update = 1,
    replace = false,
  ) {
    const unreadItem = this.unreads.value.get(contact.id)
    let unread = replace ? update : (unreadItem?.unread ?? 0) + update
    if (unread < 0) {
      logger.warn(
        `错误的未读消息数：${unread}，prev=${unreadItem?.unread ?? ''}，update=${update}, replace=${
          replace ? 'true' : 'false'
        }`,
      )
      unread = 0
    }

    if (unread === 0) {
      if (unreadItem) this.resetUnread([contact])
    } else {
      const updated = {
        ...(unreadItem ?? makeUnreadItem(contact.id, contact.type)),
        unread,
      }
      this._unreads.next(this.unreads.value.set(contact.id, updated))
    }
  }

  /**
   * 重置指定联系人的未读消息数（包括本地和线上）
   * 本地记录会立刻更新，线上记录会延迟合并更新，以尽量减少接口调用次数
   */
  private resetCache: { id: string; conversationId: string }[] | null = null
  private resetUnread(contacts: { id: string; conversationId: string }[]) {
    if (this.observingReadonly) return // 观察者只读模式下不重置未读消息数

    let updated = this.unreads.value
    contacts.forEach(contact => {
      updated = updated.remove(contact.id)
    })
    if (this.unreads.value !== updated) this._unreads.next(updated)

    // 一定时限内再次触发，会合并调用接口
    if (this.resetCache) this.resetCache.push(...contacts)
    else {
      this.resetCache = [...contacts]
      setTimeout(() => {
        void resetUnread([...(this.resetCache ?? [])])
        this.resetCache = null
      }, 1000)
    }
  }

  /**
   * 重置指定类型的联系人的未读消息数（包括本地和线上）
   * 若指定了 ownerWxid，则重置此 ownerWxid 的联系人
   */
  resetUnreadOf(type: ContactType, ownerWxid?: string) {
    if (this.observingReadonly) return // 观察者只读模式下不重置未读消息数
    const resetContactIds: { id: string; conversationId: string }[] = []
    this._unreads.next(
      this.unreads.value.filter(item => {
        if (item.type === type && (!truthy(ownerWxid) || item.ownerWxid === ownerWxid)) {
          const [, contactUserId] = splitContactId(item.contactId)
          const conversationId = makeConversationId({
            type,
            ownerWxid: item.ownerWxid,
            contactUserId,
          })
          resetContactIds.push({ id: item.contactId, conversationId })
          return false
        }
        return true
      }),
    )
    void resetUnread(resetContactIds) // 重置线上记录的未读消息数
  }

  markUnread(contactId: string) {
    const contact = this.contacts.getById(contactId)
    if (!contact) return
    void markUnread([contact])
    this.updateUnread(contact)
  }

  /**
   * ===================================
   * 维护预警记录
   * ===================================
   */

  /**
   * 记录 chat 模块发现的预警记录
   */
  private initRecordWarns() {
    this.chat.receivedWarn.subscribe(({ contact, msgId, rules }) => {
      this.markWarn(contact, msgId, rules)
    })
  }

  private markWarn(
    contact: { id: string; type?: ContactType },
    msgId: string,
    matchRules: string[],
  ) {
    // warnRules 是“增加”记录，而不是“替换”记录
    let warnItem = this.warns.value.get(contact.id) ?? makeEmptyWarnItem(contact.id, msgId)
    warnItem = {
      ...warnItem,
      count: warnItem.count + 1,
      updated: new Date().getTime(),
      matchRules: [...new Set([...warnItem.matchRules, ...matchRules])],
    }
    this._warns.next(
      // 保证最后更新的项目排序排在最前面
      OrderedMap([[contact.id, warnItem]]).concat(this.warns.value.delete(contact.id)),
    )
  }

  doneWarn(contactId: string, conversationId: string) {
    this._warns.next(this.warns.value.remove(contactId))
    void clearWarn(contactId, conversationId)

    // 向后端提交操作记录
    const contact = this.contacts.getById(contactId)
    if (contact) {
      void API('/OperationRecord/AddOperationRecord', {
        data: { type: 0, content: contact.conversationId },
      })
    }
  }
}
